对于安全框架而言，认证功能是一切的七点。所以学习 Spring Security ，就要从最基本的认证开始。


在 Spring Security 中，对认证功能做了大量的封装，只要稍微配置以下就可以使用认证功能。

## 基本认证

### 入门案例
在 Spring Boot 项目中使用 Spring Security 非常简单，创建一个新的 Spring Boot 项目，然后引入 Web 和 Spring Security 依赖即可，具体代码如下：

```xml
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
```
然后在项目中提供一个用于测试的 `/hello` 接口，代码如下：
```java
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

```
接下来启动项目，`/hello` 接口就已经被保护起来了。当用户访问 `/hello` 接口时，会自动跳转到登录页面，如图 2-1 所示，用户登录成功后，才能访问到 `/hello` 接口。

默认的登录用户名是 user，登录密码是一个随机生成的 UUID 字符串，在项目启动日志中看看到。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/img/202412212205107.png"/>
输入默认的用户名和密码，就可以成功登录了。

#### 流程分析

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/img/202412212208088.png"/>


1. 客户端（浏览器）发起请求去访问 `/hello`，这个接口是需要认证之后才能访问的。
2. 这个请求会走一遍 Spring Security 中的过滤器链，在最后的 FilterSecurityInterceptor 过滤器中被拦截下来，因为系统发现用户未认证。请求被拦截下来之后，接下来会抛出 AccessDeniedException 异常。
3. 抛出的 AccessDeniedException 异常在 ExceptionTranslationFilter 过滤器中被捕获，ExceptionTranslationFilter 过滤器通过调用 LoginUrlAuthenticationEntryPoint#commence 方法给客户端返回 302，要求客户端重定向到 `/login` 页面。
4. 客户端发送 `/login` 请求。
5. `/login` 请求被 DefaultLoginPageGenerationFilter 过滤器拦截下来，并在该过滤器中返回登录页面。所以当用户访问 `/hello` 接口时会先看到登录页面。

在整个过程中，相当于客户端一共发送了两个请求，第一个请求是  `\hello` , 服务器收到之后，返回 302，要求客户端重定向到 `\login`,于是客户端又发送了 `\login` 请求。

#### 原理分析

虽然只是引入了一个依赖，但是 Spring Boot 背后却做了很多事情：

* 开启 Spring Security 自动化配置，开启后，会自动创建一个名为 SpringSecurityFilterChain 的过滤器，并注入到 Spring 容器中，这个过滤器将负责所有的安全管理，包括用户认证、授权、重定向到登录页面等（SpringSecurityFilterChain 实际上代理了Spring Security 中的过滤器链条）。
* 创建一个 UserDetailService 实例，UserDetailService 负责提供用户数据，默认的用户数据是基于内存的用户，用户名为 user，密码是随机生成的 UUID 字符串。
* 给用户生成一个默认的登录页面。
* 开启 CSRS 攻击防御
* 开启会话固定攻击防御
* 集成 X-XSS-Protection
* 集成 X-Frame-Options 以防止单击劫持。

这里的细节很多，主要分析默认用户的生成以及默认登录页面的生成。

#### 默认用户生成
Spring Security 中定义了 UserDetail 接口来规范开发者自定义的用户对象，这样方便一些旧系统、用户表已经固定的系统集成到 Spring Security 认证体系中。

```java title='UserDetail 接口定义如下'
// 用户详细信息接口，实现序列化以便对象可以在不同环境中传递或存储。
public interface UserDetails extends Serializable {

    // 获取分配给用户的权限集合。
    Collection<? extends GrantedAuthority> getAuthorities();

    // 返回用于认证的密码。
    String getPassword();

    // 返回用户的唯一标识符，通常是用户名或邮箱地址。
    String getUsername();

    // 如果账号未过期，则返回 true。否则 false。这可以用来处理账户的有效期限。
    boolean isAccountNonExpired();

    // 如果账号未被锁定，则返回 true。否则 false。这可以用来处理账户状态（如因多次登录失败而被锁）。
    boolean isAccountNonLocked();

    // 如果凭证（如密码）未过期，则返回 true。否则 false。这可以用来确保用户定期更新他们的密码。
    boolean isCredentialsNonExpired();

    // 如果账户是激活的或启用的，则返回 true。否则 false。这可以用来启用或禁用用户账户。
    boolean isEnabled();
}
```

UserDetail 接口是用户对象的定义，负责提供用户数据源的接口是 UserDetailService ，UserDetailService 中有一个查询用户的方式。

```java
// UserDetailsService 是 Spring Security 框架中的一个接口，
// 用于加载用户特定的数据。它提供了一个方法来根据用户名获取用户信息。
public interface UserDetailsService {

    // 根据提供的用户名加载用户信息，并返回实现了 UserDetails 接口的对象。
    // 如果找不到指定的用户，则抛出 UsernameNotFoundException 异常。
    // 这个方法通常由应用程序开发者实现，以适配自己的用户存储（如数据库、LDAP等）。
    // 实现类会从持久化层查询用户数据，并将结果封装为 UserDetails 对象返回。
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
```

loadUserByUsername 有一个参数是 username ,这是用户在认证时传入的用户唯一标识，最常见的就是用户在登录表单中输入的用户名，开发者在这里拿到用户名之后，再去数据库中查询用户，最终返回一个 UserDetail 实例。

在实际项目中，一般需要开发者自定义 UserDetailService 的实现。如果开发者没有自定义 UserDetailService 的实现，Spring Security 也为 UserDetailService 提供了默认实现，如下图所示：

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/img/202412220059530.png"/>

* UserDetailsManager 在 UserDetailService 的基础上，继续定义了添加用户、更新用户、删除用户、修改密码以及判断用户是否存在共 5 种方法。
* JdbcDaoImpl 在 UserDetailsService 的基础上，通过 spring-jdbc 实现了从数据库中查询用户的方法。
* InMemoryUserDetailManager 实现了 UserDetailsManager 中关于用户的增删改查方法，不过都是基于内存的操作，数据并没有持久化。
* JdbcUserDetailsManager 继承自 JdbcDaoImpl 的同时又实现了 UserDetailsManager 接口，因此可以通过 JdbcUserDetailsManager 实现对用户的增删改改查操作，这些操作都会持久化到数据库中。不过 JdbcUserDetailsManager 有一个局限性，就是操作数据库中用户的 SQL 都是提前写好的，不够灵活，因此在实际开发中 JdbcUserDetailManager 使用的并不多。
* CachingUserDetailsService 的特点是会将 UserDetailsService 缓存起来。
* UserDetailsServiceDelegator 则是提供了 UserDetailsService 的懒加载功能。
* ReactiveUserDetailsServiceAdapter 是 webflux-web-security 模块定义的。


使用 Spring Security 时，如果仅仅是引入一个 Spring Security 依赖，则默认使用的用户就是由 InMemoryUserDetailsManager 提供的。

Spring Boot 之所以能够做到零配置使用 Spring Security，就是因为它提供了众多的自动化配置类。其中，针对 UserDetailsService 的自动化配置类是 UserDetailsServiceAutoConfiguration。

```java title='UserDetailsServiceAutoConfiguration.java'
package org.springframework.boot.autoconfigure.security.servlet;


/**
 * 自动配置类，用于在缺少某些安全组件的情况下，自动创建一个基于内存的用户详情服务。
 * 该配置仅在应用上下文中存在认证管理器或对象后处理器，并且缺失认证管理器、认证提供者、用户详情服务或认证管理解析器等bean时激活。
 */
@AutoConfiguration // 指示这是一个自动配置类
@ConditionalOnClass({AuthenticationManager.class}) // 当classpath中存在指定类时生效
@ConditionalOnBean({ObjectPostProcessor.class}) // 当容器中存在指定类型的bean时生效
@ConditionalOnMissingBean(
    value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class},
    type = {"org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository"}
)
public class UserDetailsServiceAutoConfiguration {

    /** 定义了未加密密码的前缀 */
    private static final String NOOP_PASSWORD_PREFIX = "{noop}";

    /** 匹配带算法标识的密码模式 */
    private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");

    /** 日志记录器，用于记录日志信息 */
    private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

    /**
     * 构造函数，初始化UserDetailsServiceAutoConfiguration实例。
     */
    public UserDetailsServiceAutoConfiguration() {
    }

    /**
     * 创建一个基于内存的用户详情服务管理器。
     *
     * @param properties SecurityProperties对象，包含安全相关的配置属性。
     * @param passwordEncoder 密码编码器提供者，用于获取可能存在的密码编码器。
     * @return InMemoryUserDetailsManager实例，用于管理内存中的用户详情。
     */
    @Bean
    @Lazy // 延迟初始化bean，直到首次使用时才创建。
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
        SecurityProperties.User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(new UserDetails[]{User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
    }

    /**
     * 获取用户的实际密码或推断出的默认密码。
     *
     * @param user 用户配置信息。
     * @param encoder 可选的密码编码器。
     * @return 返回带有适当前缀的密码字符串，如果密码未加密则添加"{noop}"前缀。
     */
    private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
        String password = user.getPassword();
        if (user.isPasswordGenerated()) {
            logger.warn(String.format("%n%nUsing generated security password: %s%n%nThis generated password is for development use only. Your security configuration must be updated before running your application in production.%n", user.getPassword()));
        }

        // 如果没有提供密码编码器并且密码不匹配预定义模式，则认为是明文密码，添加"{noop}"前缀。
        return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
    }
}
```

从上述代码可以看出，有两个比较重要的条件促使系统自动提供一个 InMemoryUserDetailsManager 的实例：
1. 当前 classpath 下存在 AuthenticationManager 类。
2. 当前项目中，系统没有提供 AuthenticationManager、AuthenticationProvider、UserDetailsService 以及 ClientRegistrationRepository 实例。

默认情况下，上面的条件都满足，此时 Spring Security 会提供一个 InMemoryUserDetailsManager 实例。

用户数据来源于 SecurityProperty#getUser 方法：

```java
package org.springframework.boot.autoconfigure.security;



/**
 * 安全属性配置类，用于配置与Spring Security相关的设置。
 */
@ConfigurationProperties(
    prefix = "spring.security" // 配置属性前缀，表示在application.properties或application.yml文件中定义安全属性时使用的前缀。
)
public class SecurityProperties {

    /** Basic认证过滤器的默认排序值 */
    public static final int BASIC_AUTH_ORDER = 2147483642;

    /** 忽略路径模式匹配过滤器的排序值 */
    public static final int IGNORED_ORDER = Integer.MIN_VALUE;

    /** 默认的过滤器排序值 */
    public static final int DEFAULT_FILTER_ORDER = -100;

    /**
     * 过滤器配置对象，包含关于安全过滤器链的配置信息。
     */
    private final Filter filter = new Filter();

    /**
     * 用户配置对象，包含有关用户认证的信息。
     */
    private final User user = new User();

    /**
     * 构造函数，初始化SecurityProperties实例。
     */
    public SecurityProperties() {
    }

    /**
     * 获取用户配置对象。
     *
     * @return 返回一个User实例，包含用户名、密码等信息。
     */
    public User getUser() {
        return this.user;
    }

    /**
     * 获取过滤器配置对象。
     *
     * @return 返回一个Filter实例，包含过滤器链的排序和分派类型。
     */
    public Filter getFilter() {
        return this.filter;
    }

    /**
     * 用户配置类，用于存储用户的认证信息。
     */
    public static class User {
        /** 默认用户名 */
        private String name = "user";

        /** 用户密码，默认为随机生成 */
        private String password = UUID.randomUUID().toString();

        /** 用户角色列表 */
        private List<String> roles = new ArrayList<>();

        /** 标记是否使用了默认生成的密码 */
        private boolean passwordGenerated = true;

        /**
         * 构造函数，初始化User实例。
         */
        public User() {
        }

        /**
         * 获取用户名。
         *
         * @return 返回用户名字符串。
         */
        public String getName() {
            return this.name;
        }

        /**
         * 设置用户名。
         *
         * @param name 要设置的用户名。
         */
        public void setName(String name) {
            this.name = name;
        }

        /**
         * 获取用户密码。
         *
         * @return 返回用户密码字符串。
         */
        public String getPassword() {
            return this.password;
        }

        /**
         * 设置用户密码。如果提供了非空密码，则标记passwordGenerated为false。
         *
         * @param password 要设置的用户密码。
         */
        public void setPassword(String password) {
            if (StringUtils.hasLength(password)) {
                this.passwordGenerated = false;
                this.password = password;
            }
        }

        /**
         * 获取用户角色列表。
         *
         * @return 返回用户角色列表。
         */
        public List<String> getRoles() {
            return this.roles;
        }

        /**
         * 设置用户角色列表。
         *
         * @param roles 角色列表。
         */
        public void setRoles(List<String> roles) {
            this.roles = new ArrayList<>(roles);
        }

        /**
         * 检查是否使用了默认生成的密码。
         *
         * @return 如果使用了默认生成的密码返回true，否则返回false。
         */
        public boolean isPasswordGenerated() {
            return this.passwordGenerated;
        }
    }

    /**
     * 过滤器配置类，用于指定安全过滤器的行为。
     */
    public static class Filter {
        /** 过滤器的排序值，默认为-100 */
        private int order = -100;

        /** 分派类型集合，默认包括异步、错误和请求分派 */
        private Set<DispatcherType> dispatcherTypes;

        /**
         * 构造函数，初始化Filter实例，并设置默认分派类型。
         */
        public Filter() {
            this.dispatcherTypes = new HashSet<>(Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
        }

        /**
         * 获取过滤器排序值。
         *
         * @return 返回过滤器排序值。
         */
        public int getOrder() {
            return this.order;
        }

        /**
         * 设置过滤器排序值。
         *
         * @param order 要设置的排序值。
         */
        public void setOrder(int order) {
            this.order = order;
        }

        /**
         * 获取分派类型集合。
         *
         * @return 返回分派类型集合。
         */
        public Set<DispatcherType> getDispatcherTypes() {
            return this.dispatcherTypes;
        }

        /**
         * 设置分派类型集合。
         *
         * @param dispatcherTypes 要设置的分派类型集合。
         */
        public void setDispatcherTypes(Set<DispatcherType> dispatcherTypes) {
            this.dispatcherTypes = dispatcherTypes;
        }
    }
}
```
从 SecurityProperties.User 类中，可以看出默认的用户名是 user，默认的密码是一个 UUID 字符串，再回到 InMemoryUserDetailsManager 方法中，构造 InMemoryUserDetailsManager 实例时需要一个 User 对象。这里的 User 对象不是 SecurityProperties.User。

根据源码可以看出，只要对 Spring Boot 中的 application.yml 配置文件中添加如下配置，就能定制 SecurityProperties.User 类中各属性的值。

```java
spring:
  security:
    user:
      name: wangkaiqi
      password: 123456
      roles: role_admin,role_user
```

### 默认页面生成
上面案例一共存在两个默认页面，登录页面（login）和注销登录页面（/logout）。

这里刨析一下这两个页面的来源，Spring Security 常见的过滤器中，和页面相关的过滤器有 DefaultLoginPageGenerationFilter 和 DefaultLogoutPageGeneratingFilter。

通过过滤器的名字可以分辨出 DefaultLoginPageGenerationFilter 过滤器用来生成默认的登录页面，DefaultLogoutPageGenerationFilter 过滤器则用来生成默认的注销页面。

在第一次请求 `/hello` 接口的时候，就会先经过 DefaultLoginPageGenerating 过滤器，但是由于 `/hello` 接口与登录无关，因此 DefaultLoginPageGeneratingFilter 过滤器并未干涉 `/hello` 接口，等到第二次重定向到 `/login` 页面的时候，就和 DefaultLoginPageGeneratingFilter 有关系了，此时请求就会在 DefaultLoginPageGeneratingFilter 中进行处理，生成登录页面返回给客户端。


```java title='DefaultLoginPageGeneratingFilter.java'
/**
 * 默认登录页面生成过滤器，用于动态生成登录页面HTML。
 * 当没有配置自定义登录页面时，此过滤器会检查请求是否是针对登录页面，并响应一个包含表单的HTML页面。
 */
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {

    /**
     * 过滤器的主要逻辑，处理传入的请求。
     *
     * @param request 请求对象。
     * @param response 响应对象。
     * @param chain 过滤器链。
     * @throws IOException 如果发生I/O错误。
     * @throws ServletException 如果发生Servlet错误。
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
    }

    /**
     * 处理HTTP请求，根据请求条件生成并发送登录页面HTML。
     *
     * @param request HTTP请求对象。
     * @param response HTTP响应对象。
     * @param chain 过滤器链。
     * @throws IOException 如果发生I/O错误。
     * @throws ServletException 如果发生Servlet错误。
     */
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        boolean loginError = this.isErrorPage(request);
        boolean logoutSuccess = this.isLogoutSuccess(request);

        // 如果不是登录页面请求，也不是错误页面或注销成功的请求，则继续过滤器链
        if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
            chain.doFilter(request, response);
        } else {
            // 生成登录页面HTML并写入响应
            String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().write(loginPageHtml);
        }
    }

    // 省略了其他辅助方法如isErrorPage、isLogoutSuccess、isLoginUrlRequest和generateLoginPageHtml等的实现。
}
```

```java
package org.springframework.security.web.authentication.ui;


/**
 * 默认注销页面生成过滤器。
 * 用于动态生成一个简单的HTML注销确认页面，当用户访问特定的URL（例如/logout）时，显示给用户以确认是否要执行注销操作。
 */
public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter {

    /**
     * 请求匹配器，默认匹配GET请求到/logout路径。
     */
    private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET");

    /**
     * 用于解析隐藏输入字段的函数，默认返回空Map。
     */
    private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = (request) -> {
        return Collections.emptyMap();
    };

    /**
     * 构造函数，初始化过滤器实例。
     */
    public DefaultLogoutPageGeneratingFilter() {
    }

    /**
     * 过滤器的主要逻辑，处理传入的请求。
     *
     * @param request HTTP请求对象。
     * @param response HTTP响应对象。
     * @param filterChain 过滤器链。
     * @throws ServletException 如果发生Servlet错误。
     * @throws IOException 如果发生I/O错误。
     */
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (this.matcher.matches(request)) {
            // 如果请求匹配，则渲染注销页面
            this.renderLogout(request, response);
        } else {
            // 如果不匹配且日志级别允许，则记录未渲染注销页面的原因
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Did not render default logout page since request did not match [%s]", this.matcher));
            }
            // 继续过滤器链
            filterChain.doFilter(request, response);
        }
    }

    /**
     * 渲染注销页面的方法。
     *
     * @param request HTTP请求对象。
     * @param response HTTP响应对象。
     * @throws IOException 如果发生I/O错误。
     */
    private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
        StringBuilder sb = new StringBuilder();
        // 构建HTML页面字符串
        // 设置响应内容类型并写入构建好的HTML字符串
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(sb.toString());
    }

    /**
     * 设置解析隐藏输入字段的函数。
     *
     * @param resolveHiddenInputs 解析隐藏输入字段的函数。
     */
    public void setResolveHiddenInputs(Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs) {
        Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null");
        this.resolveHiddenInputs = resolveHiddenInputs;
    }

    /**
     * 渲染隐藏输入字段的方法。
     *
     * @param request HTTP请求对象。
     * @return 返回包含隐藏输入字段的HTML字符串。
     */
    private String renderHiddenInputs(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        Iterator<Map.Entry<String, String>> iterator = ((Map<String, String>)this.resolveHiddenInputs.apply(request)).entrySet().iterator();

        while(iterator.hasNext()) {
            Map.Entry<String, String> input = iterator.next();
            sb.append("<input name=\"").append(input.getKey())
              .append("\" type=\"hidden\" value=\"").append(input.getValue()).append("\" />\n");
        }

        return sb.toString();
    }
}
```

## 登录表单配置

首先创建一个新的 Spring Boot 项目，引入 Web 和 Spring Security 依赖。

项目创建好之后，为了方便测试，在 application.yml 中添加如下配置，将登录名和密码固定下来：

```yml
spring:
  security:
    user:
      name: wangkaiqi
      password: 123456
      roles: role_admin,role_user
```

接下来，在 resources/static 目录下创建一个 login.html 页面，作为自定义的登录页面。

```html title='login.html'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9C9C9C;
        background-color: #EAEAEA;
    }
</style>
<body>
<div id="login">
    <div class="container">
        <div id="login-row" class="row justify-content-center align-items-center">
            <div id="login-column" class="col-md-6">
                <div id="login-box" class="col-md-12">
                    <form id="login-form" class="form" action="/doLogin" method="post">
                        <h3 class="text-center text-info">登录</h3>
                        <div class="form-group">
                            <label for="username" class="text-info">用户名:</label><br>
                            <input type="text" name="uname" id="username" class="form-control">
                        </div>
                        <div class="form-group">
                            <label for="password" class="text-info">密码:</label><br>
                            <input type="text" name="passwd" id="password" class="form-control">
                        </div>
                        <div class="form-group">
                            <input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
```
login.html 的核心内容就是一个登录表单，接下来定义两个测试接口，作为受保护的资源。当用户登录成功之后，就可以访问受保护的资源。


```java title='测试接口'
@RestController
public class LoginController {
    @RequestMapping("/index")
    public String index() {
        return "login success";
    }
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}
```
最后提供一个 Spring Security 的配置类：

```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .authorizeHttpRequests((requests) -> requests
                        .anyRequest().authenticated()
                )
                .formLogin((form) -> form
                        .loginPage("/login.html")
                        .loginProcessingUrl("/doLogin")
                        .defaultSuccessUrl("/index")
                        .failureHandler(new MyAuthenticationFailureHandler())
                        .usernameParameter("uname")
                        .passwordParameter("passwd")
                        .permitAll()
                )
                .csrf((csrf) -> csrf.disable())
                .build();
    }
}
```

 authorizeHttpRequests 方法标识开启权限配置，.anyRequest().authenticated() 表示所有的请求都要认证之后才能访问。

formLogin() 表示开启表单登录配置，loginPage 用来配置登录页面地址；loginProcessingUrl 用来配置登录接口地址；defaultSuccessUrl 表示登录成功后的跳转地址；

usernameParameter 表示登录用户名的参数名称；passwordParameter 表示登录密码的参数名称；permitAll 表示跟登录相关的页面和接口不做拦截，直接通过。

loginProcessingUrl、usernameParameter、passwordParameter 需要和 login.html 中登录表单的配置一致。

最后的 csrf((csrf) -> csrf.disable()) 表示禁用 CSRF 防御功能。


配置完成后，启动 Spring Boot 项目，访问 index，会自动跳转至 login.html 页面。

输入账号密码，登录成功后，就可以访问受保护的资源了。

### 配置细节

前面配置中，用 defaultSuccessUrl 表示用户登录成功的跳转地址，用 failureUrl 表示用户登录失败后的跳转地址。

除了这两种方法可以配置之外，还有其它方法也可以配置。

#### 登录成功
当用户登录成功之后，除了 defaultSuccessUrl 可以实现登录成功后的跳转之后，successForwardUrl 也可以实现。

无论是 defaultSuccessUrl 还是 successForwardUrl ,最终所配置的都是 AuthenticationSuccessHandler 接口的实例。

Spring Security 中专门提供了 AuthenticationSuccessHandler 接口来处理登录成功事项：


```java title='AuthenticationSuccessHandler.java'
public interface AuthenticationSuccessHandler {
    default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        this.onAuthenticationSuccess(request, response, authentication);
        chain.doFilter(request, response);
    }

    void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
}
```
AuthenticationSuccessHandler 接口中一共定义了两个方法，其中一个是 default 方法，处理特定的认证请求 Authentication Filter 中会用到；另外一个是非 default 方法，用来处理登录成功的具体事项。其中 request 和 response 参数好理解，authentication 参数保存了登录成功的用户信息。

AuthenticationSuccessHandler 接口共有三个实现类
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/img/202412221453290.png"/>

1. SimpleUrlAuthenticationSuccessHandler 继承自 AbstractAuthenticationTargetUrlRequestHandler ，通过 AbstractAuthenticationTargetUrlRequestHandler 中的 handle 方法实现请求重定向。

2. SavedRequestAwareAuthenticationSuccessHandler 在 SimpleUrlAuthenticationSuccessHandler 的基础上增加了请求缓存的功能，可以记录之前请求的地址，进而在登录成功后重定向到一开始访问的地址。

3. ForwardAuthenticationSuccessHandler 的实现比较简单，就是一个服务端跳转。


```java title='SavedRequestAwareAuthenticationSuccessHandler.java'
public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    protected final Log logger = LogFactory.getLog(this.getClass());
    private RequestCache requestCache = new HttpSessionRequestCache();

    public SavedRequestAwareAuthenticationSuccessHandler() {
    }

    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        SavedRequest savedRequest = this.requestCache.getRequest(request, response);
        if (savedRequest == null) {
            super.onAuthenticationSuccess(request, response, authentication);
        } else {
            String targetUrlParameter = this.getTargetUrlParameter();
            if (!this.isAlwaysUseDefaultTargetUrl() && (targetUrlParameter == null || !StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
                this.clearAuthenticationAttributes(request);
                String targetUrl = savedRequest.getRedirectUrl();
                this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
            } else {
                this.requestCache.removeRequest(request, response);
                super.onAuthenticationSuccess(request, response, authentication);
            }
        }
    }

    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }
}
```

这里的核心方法就是onAuthenticationSuccess
1. 首先从requestCache中获取缓存下来的请求，如果没有获
取到缓存请求，就说明用户在访问登录页面之前并没有访问其他页
面，此时直接调用父类的onAuthenticationSuccess方法来处理，最
终会重定向到defaultSuccessUrl指定的地址。
2. 接下来会获取一个targetUrlParameter，这个是用户显式
指定的、希望登录成功后重定向的地址，例如用户发送的登录请求是
http://localhost:8080/doLogin?target=/hello，
这就表示当用户登录成功之后，希望自动重定向到/hello这个接口。
getTargetUrlParameter就是要获取重定向地址参数的key，也就是
上面的target，拿到target之后，就可以获取到重定向地址了。
3. 如果targetUrlParameter 存 在 ， 或者用户设置了
alwaysUseDefaultTargetUrl为true，这个时候缓存下来的请求就没
有意义了。此时会直接调用父类的onAuthenticationSuccess方法完
成 重定向。targetUrlParameter 存 在 ， 则 直 接 重 定 向 到
targetUrlParameter指定的地址；alwaysUseDefaultTargetUrl为
true ，则直接重定向到defaultSuccessUrl指定的地址；如果
targetUrlParameter 存 在 并 且 alwaysUseDefaultTargetUrl 为
true，则重定向到defaultSuccessUrl指定的地址。
4. 如果前面的条件都不满足，那么最终会从缓存请求
savedRequest中获取重定向地址，然后进行重定向操作。


当然，开发者也可以自定义自己的 SavedRequestAwareAuthenticationSuccessHandler




AuthenticationSuccessHandler默认的三个实现类，无论是哪
一个，都是用来处理页面跳转的。有时候页面跳转并不能满足我们的
需求，特别是现在流行的前后端分离开发中，用户登录成功后，就不
再需要页面跳转了，只需要给前端返回一个JSON数据即可，告诉前
端登录成功还是登录失败，前端收到消息之后自行处理。像这样的需
求，我们可以通过自定义AuthenticationSuccessHandler的实现类
来完成

```java
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        Map<String, Object> resp = new HashMap<>();
        resp.put("status", 200);
        resp.put("msg", "登录成功!");
        ObjectMapper om = new ObjectMapper();
        String s = om.writeValueAsString(resp);
        response.getWriter().write(s);
    }
}
```

在自定义的MyAuthenticationSuccessHandler 中 ， 重 写
onAuthenticationSuccess 方 法 ， 在 该 方 法 中 ， 通 过
HttpServletResponse对象返回一段登录成功的JSON字符串给前端
即 可 。 最 后 ， 在 SecurityConfig 中 配 置 自 定 义 的
MyAuthenticationSuccessHandler，代码如下：

```java
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        Map<String, Object> resp = new HashMap<>();
        resp.put("status", 200);
        resp.put("msg", "登录成功!");
        ObjectMapper om = new ObjectMapper();
        String s = om.writeValueAsString(resp);
        response.getWriter().write(s);
    }
}

```
#### 登录失败

接下来看登录失败的处理逻辑。为了方便在前端页面展示登录失
败的异常信息，我们首先在项目的pom.xml文件中引入thymeleaf依
赖，代码如下：
```xml
 <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
```
然后在resources/templates目录下新建mylogin.html，代码如
下

```html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9C9C9C;
        background-color: #EAEAEA;
    }
</style>
<body>
<div id="login">
    <div class="container">
        <div id="login-row" class="row justify-content-center align-items-center">
            <div id="login-column" class="col-md-6">
                <div id="login-box" class="col-md-12">
                    <form id="login-form" class="form" action="/doLogin" method="post">
                        <h3 class="text-center text-info">登录</h3>
                        <div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
                        <div class="form-group">
                            <label for="username" class="text-info">用户名:</label><br>
                            <input type="text" name="uname" id="username" class="form-control">
                        </div>
                        <div class="form-group">
                            <label for="password" class="text-info">密码:</label><br>
                            <input type="text" name="passwd" id="password" class="form-control">
                        </div>
                        <div class="form-group">
                            <input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
```
mylogin.html和前面的login.html基本类似，前面的login.html
是静态页面，这里的mylogin.html是thymeleaf模板页面，
mylogin.html页面在form中多了一个div，用来展示登录失败时候的
异常信息，登录失败的异常信息会放在request中返回到前端，开发
者可以将其直接提取出来展示。
既然mylogin.html是动态页面，就不能像静态页面那样直接访问
了，需要我们给mylogin.html页面提供一个访问控制器：




## 登录用户数据获取


## 用户的定义方式
